课程文档:http://docs.vikingship.xyz/,尚硅谷讲解vue3+ts文档:https://24kcs.github.io/vue3_study/
这里内容很多,但不是TS的全部,还有高级用法,在官方文档里面可以看。不过先将这些功能用起来就差不多了,面试就够了,面试的时候把各种用法说一下就行了,别人的问题不会超过这个文档的范围,如果超过了,回答不出来再说,实际应用时更复杂的用法等遇到了就去查找文档。
TS居然有汉语提示,我写了这么多种代码,就没见到过汉语提示,你说TS好不好用。(光学习的话会觉得这个typescript很好用,但是实际做项目的时候,定义起来是非常复杂的,所以要知道更重要的是逻辑,做项目就是要先把活做完,这种提示等到我真正做项目的时候,我是恨不得把这个提示关掉的,如果是自己定义的还好,如果是多人合作,就更加痛苦了,改别人的代码别人是会拼命的,所以痛苦)

1、关于在项目中使用typescript的一些思考:
vue3对typescript的支持有了很大的提升,但是用起来即使是高手也有一些困扰,类型定义确实复杂,偶然间搜索到一个问答,里面讲了vue3对typescript的支持,有一个回答我觉得很好,能够解决我的一些心理负担。

https://www.zhihu.com/question/453332049。尤雨溪亲自作答。这个还是要看项目规定,如果别人硬是要每个数据类型都定义清楚,那也没有办法,多花点时间去定义;如果不那么严格,就在难定义的地方使用any类型,先把业务逻辑走通,项目完成之后,可以花一些时间来专门定义类型。
2、使用typescript的难度层级
我自己总结一下使用typescript的难度层级,一级一级的递增。(这个就可以在面试的时候和别人聊一聊,就算说的不好,听一下别人的意见也是很好的,也许别人要的就是这样的真实的总结)
①简单地使用原始类型,定义一下number或string等数据类型,其余的一概用any类型。
②可以使用typescript来定义后端返回数据的类型。
③对第三方库的元素进行数据类型定义,这部分比较麻烦,因为很多不一定有文档,有了文档不一定有types库来安装,比如说echarts有文档,但别的第三方库不一定有。
什么是TypeScript?



TypeScript相比JS优势:

Typescript 官网地址: https://www.typescriptlang.org/zh/
使用 nvm 来管理 node版本: https://github.com/nvm-sh/nvm

安装Typescript:
xxxxxxxxxx11npm install -g typescript执行ts文件:

方式一:使用tsc全局命令,将ts文件转译为js文件后,执行js文件:
xxxxxxxxxx51# 查看 tsc 版本2tsc -v34# 编译 ts 文件,使文件编译为js文件,然后可以使用 node filename.js 命令来执行这个文件5tsc fileName.ts方式二:安装ts-node,直接执行ts文件:
xxxxxxxxxx31npm install -g ts-node23# 这样就可以使用 ts-node 文件名.ts 来执行文件了(webstorm上配置在webstrom快捷键.md文件中有)








主要就是ts-loader来解析项目里面的ts代码,将其转换为js代码,其余的和js项目一样。
参考这里:https://blog.csdn.net/wu5229485/article/details/94721056
xxxxxxxxxx61yarn add -D typescript2yarn add -D webpack webpack-cli3yarn add -D webpack-dev-server4yarn add -D html-webpack-plugin clean-webpack-plugin5yarn add -D ts-loader6yarn add -D cross-envxxxxxxxxxx31// import './01_helloworld'23document.write('Hello Webpack TS!')xxxxxxxxxx1212<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <meta http-equiv="X-UA-Compatible" content="ie=edge">7 <title>webpack & TS</title>8</head>9<body>10 11</body>12</html>xxxxxxxxxx531const {CleanWebpackPlugin} = require('clean-webpack-plugin')2const HtmlWebpackPlugin = require('html-webpack-plugin')3const path = require('path')45const isProd = process.env.NODE_ENV === 'production' // 是否生产环境67function resolve (dir) {8 return path.resolve(__dirname, '..', dir)9}1011module.exports = {12 mode: isProd ? 'production' : 'development',13 entry: {14 app: './src/main.ts'15 },1617 output: {18 path: resolve('dist'),19 filename: '[name].[contenthash:8].js'20 },2122 module: {23 rules: [24 {25 test: /\.tsx?$/,26 use: 'ts-loader',27 include: [resolve('src')]28 }29 ]30 },3132 plugins: [33 new CleanWebpackPlugin({34 }),3536 new HtmlWebpackPlugin({37 template: './public/index.html'38 })39 ],4041 resolve: {42 extensions: ['.ts', '.tsx', '.js']43 },4445 devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',4647 devServer: {48 host: 'localhost', // 主机名49 stats: 'errors-only', // 打包日志输出输出错误信息50 port: 8081,51 open: true52 },53}xxxxxxxxxx21"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",2"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"xxxxxxxxxx21yarn dev2yarn buildTypescript 文档地址:Basic Types:
Boolean Null Undefined Number String BigInt Symbol Void Array Tuple Enum Unknown Any Never Object
TS中原始数据类型的值不可被修改为其它类型。
typescript原始数据类型 - primitive values:
- Boolean
- Null
- Undefined
- Number
- String
- BigInt
- Symbol
- void (有的人将这个类型也加入到原始类型中,但这门课的老师并没有加入,先放这里吧)
xxxxxxxxxx191let isDone: boolean = false2isDone = 123 // 这句话会报错34// 接下来来到 number,注意 es6 还支持2、8、16进制,让我们来感受下56let age: number = 107let binaryNumber: number = 0b111189// 之后是字符串,注意es6新增的模版字符串也是没有问题的10let firstName: string = 'viking'11let message: string = `Hello, ${firstName}, age is ${age}`1213// 还有就是两个奇葩兄弟两,undefined 和 null14let u: undefined = undefined15let n: null = null1617// 注意 undefined 和 null 是所有类型(当然包括它们自己)的子类型。18// 那么在这里就是说 undefined 类型的变量,可以赋值给 number 类型的变量(需要将tsconfig.json文件中的严格模式关闭):19let num: number = undefined2024.01.02
undefined和null是所有类型的子类型,但是如果直接赋值给number类型, 还是会有提示:
typescript版本是5.3.2。可能原因是我使用的是typescript-playground,文件是tsx类型,而不是ts类型,可能会有部分不同。
指定any(任意)类型,值可以被修改为任意类型。
xxxxxxxxxx391let notSure: any = 42notSure = 'maybe it is a string'3notSure = 'boolean'4// 在任意类型的值上访问任何属性都是允许的:如果属性不存在,就输出 undefined5notSure.myName6// 也允许调用任何方法。(只是说明这样写了ts不会报错,但是真正执行的时候是会报错的,会报这个方法不是函数)7notSure.getName()89// 当一个数组中要存储多个数据,个数和类型都不确定,此时可以使用any类型来定义数组10let arr: any[] = [123,'ok',true]11console.log(arr);//输出: [ 123, 'ok', true ]1213// 一般情况下,数组里面的数据都是同一类型,复杂就复杂在里面是对象或者数组,这时候直接定义成这样就好了。里面的对象或者数组具体是什么样子的,就不需要定义了,而且也不需要进行定义,否则不是太复杂了吗。14let arr: object[] = [15 {16 id:1,17 name:'lily'18 },19 {20 id:2,21 name:'jack'22 }23]24console.log(arr);// 输出:[ { id: 1, name: 'lily' }, { id: 2, name: 'jack' } ]2526// 二维数组的定义27// 方式一:28let arr: number[][] = [29 [1,2,3],30 [4,5,6]31]32console.log(arr);// 输出:[ [ 1, 2, 3 ], [ 4, 5, 6 ] ]3334// 方式二:35let arr: Array<Array<any>> = [36 [1, 2, 3],37 ["4", "5", "6"],38]39console.log(arr);// 输出:[ [ 1, 2, 3 ], [ '4', '5', '6' ] ]2024.01.02
上面的 object[] 这种类型,可以设置为更具体的类型:
xxxxxxxxxx151type person = {2id:number3name:string4}56let arr:person[] = [7{8id:1,9name:"tom"10},11{12id:2,13name:"jerry"14}15]
Array<Array<any>>,我知道这是泛型的指定类型的方式,但是这里的Array表示的是什么呢?从提示可以知道,这里的Array是interface:
表示没有任何类型,当一个函数没有返回值时,通常会见到其返回值是void。
xxxxxxxxxx31function fn(): void {2 console.log('fn()');3}表示非原始类型,也就是除number、string、boolean等之外的类型。使用object类型,就可以更好地表示像Object.create这样的API。
xxxxxxxxxx141//定义一个函数,参数是object类型,返回值也是object类型2function getObj(obj: object): object {3 console.log(obj);4 return{5 name:'ok',6 age:187 }8}910console.log(getObj({name:'ink',age:11}));11/*输出结果:12{ name: 'ink', age: 11 }13{ name: 'ok', age: 18 }14*/这里我发现一个问题,就是如果obj定义为object类型,在函数中使用obj.name和obj.age的话,会报错。
不仅仅是标红,执行也会报错。原因是什么呢?因为是object类型里面没有name和age属性,所以不能使用。说明object类型不能在这种情况下使用。那么该怎么办呢?
使用接口。
执行OK。
表示取值可以为多种类型中的一种。
需求1:定义一个函数,得到一个数字或字符串的字符串形式
xxxxxxxxxx141function toStr(x: number | string): string {2 return x.toString()3}45function toStr(x: number | string): number | string {6 return x.toString()7}89console.log(toStr('123'))10console.log(toStr(123))11/*输出结果:121231312314*/需求2:定义一个函数,得到一个数字或字符串的长度
xxxxxxxxxx381function toStr(x: number | string): number {2 // string类型本身就有length属性3 if(x.length){4 return x.length5 }else{6 return x.toString().length7 }8}910console.log(toStr('123'))11console.log(toStr(123))12/*输出结果:13314315*/1617//虽然代码可以正常执行,但是ts会报错,因为不知道x到底是string还是number,需要用到类型断言。18// 类型断言写法一:19function toStr(x:number | string):number {20 //先对str进行断言,说它就是string类型,然后if(str.length)这句代码才成立。21 const str = x as string22 23 if(str.length){24 return str.length25 }else{26 const num = x as number27 return num.toString().length28 }29}30 31// 类型断言写法二:32function toStr(x:number | string):number {33 if((<string>x).length){34 return (<string>x).length35 }else{36 return (<number>x).toString().length37 }38}2024.01.02
注意:上面类型断言的第二种写法,不要和泛型的写法弄混了,眼睛一晃很容易弄混。
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2-2章节其实就是数据类型的基础,有原始数据类型、any、void、object、联合数据类型,后面章节的数据类型其实可以称为“组合类型”,这个名称是我自己取的,本来就可以这么理解。
这一点明确了,后面的内容就好理解了,用起来就很方便了。
Typescript 文档地址:Array 和 Tuple
定义数组类型:
xxxxxxxxxx181// 定义方法1:2// 最简单的方法是使用「类型 + 方括号」来表示数组:3const arrOfNumbers: number[] = [1, 2, 3, 4]45// 定义方法2:6// 数组定义的泛型的写法:let 变量名:Array<数据类型> = [值1,值2,值3]7const arr: Array<number> = [1,2,3]89//数组的项中不允许出现其他的类型:10//数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:11arrOfNumbers.push(3)12arrOfNumbers.push('abc') //报错1314// 那如果数组里面的数据有多种类型怎么办呢?可以使用联合类型15// 泛型写法16const arr : Array<number | string> = [1,'2',3] // 不报错17// 简单写法18const arr : (number | string)[] = [5,'6',7] // 不报错定义元组类型:
xxxxxxxxxx121// 元组的表示和数组非常类似,只不过它将 类型写在了里面 ,这就对每一项起到了限定的作用2let user: [string, number] = ['viking', 20]34//注意:定义的时候,当我们写少一项就会报错,同样写多一项也会有问题。5const user : [string, number] = ['viking', 20, 18] // 报错67// 直接赋值时,元组长度超过或少于2个,不行。8user = ['molly', 20, true] // 报错,但可以用push方法向里面添加元素910// 元组可以使用数组的方法,来为元组中添加元素,但元素类型只能是指定的类型之一,这样元组定义的类型就会自动扩充,因为上面已经讲了,元组数据类型的位置和数据的个数,必须和定义时的类型和个数保持一致:11user.push(123) //可以添加12user.push(true) //不能添加为什么要指定类型呢?因为后面的操作需要固定类型。那么别人在使用的时候,不需要等到运行阶段才发现错误,而是在编写代码的阶段就监视别人的使用状态,做出预警,这就提高了效率,免得别人到处找原因。
下面是一个反面例子。
xxxxxxxxxx31// 如果定义为any类型,就会让别人使用部分方法时,考虑不周全2let arr: any[] = [123,'ok',true]3console.log(arr[1].split(''));//这里的意思是为arr中序号为1的数据进行split操作,如果我定义成了any类型,那么arr这个数组中序号为1的数据可以是任意的,但split()操作会出错。所以慎用any类型。2024.01.02
二维元组类型可以这样定义:
xxxxxxxxxx11let arr:[string,number[]] = ['tom',[1,2,3]];还是需要赋值的时候,类型和个数严格对应。
Typescript 文档地址:Interface
Duck Typing 概念:
如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。
xxxxxxxxxx371// 我们定义了一个接口 Person,注意里面的属性没有分隔符,但是写 ; 或 , 分隔居然不报错,官方文档用的是分号,我不写的话,prettier会自动在后面加上分号,说明这里写 ; 才是正确的。2interface Person {3 name: string;4 age: number;5}67// 接着定义了一个变量 viking,它的类型是 Person。这样,我们就约束了 viking 的形状必须和接口 Person 一致。8let viking: Person ={ 9 name: 'viking',10 age: 2011}1213//有时我们希望不要完全匹配一个形状,那么可以用可选属性(意思是属性可省略的):(就是在属性名后面加一个?)14interface Person {15 name: string;16 age?: number; //可选属性17}1819let viking: Person = {20 name: 'Viking'21}2223//接下来还有只读属性,有时候我们希望对象中的一些字段只能在创建的时候被赋值,后面就不能被修改,那么可以用 readonly 定义只读属性。2425interface Person {26 readonly id: number;//只读属性27 name: string;28 age?: number;29}3031let viking: Person = {32 id: 1, // 这是赋初始值,在创建的时候被赋值,但后面就不能修改了。33 name: 'ok',34 age: 2035}3637viking.id = 9527;//报错
Typescript 文档地址:Functions
函数类型:通过接口的方式作为函数的类型来使用。为了使用接口表示函数类型,需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里面的每一个参数都需要名字和类型。
xxxxxxxxxx141//定义一个接口,用来作为某个函数的类型使用2interface ISearchFunc {3 //定义一个调用签名4 (source: string, subString: string): boolean;5}67// 定义一个函数,该类型就是上面定义的接口8// 这个例子将函数左右两边的类型都定义了,其实可以只定义一边,下面的图中有说明。9const searchString: ISearchFunc = function (source: string, subString: string): boolean {10 return source.search(subString) > -1;11};1213//调用函数14console.log(searchString("你好啊,今天", "天")); // true

xxxxxxxxxx681// 和js一样,ts中定义函数有命名函数(函数声明)和匿名函数(函数表达式)两种定义方式,只是ts要求类型声明23// 来到我们的第一个例子,约定输入,约定输出4function add(x: number, y: number): number {5 return x + y6}78// 可选参数用 ? 进行修饰9function add(x: number, y: number, z?: number): number {10 if (typeof z === 'number') {11 return x + y + z12 } else {13 return x + y14 }15}161718//参数默认值,直接在参数后面指定19const getFullName = function (firstName: string = "湖",lastName?: string): string {20 if (lastName) {21 return firstName + lastName;22 } else {23 return firstName;24 }25};26const p = getFullName("沪", "路");27// 2024.01.02 但是这种参数默认值的指定方式,阅读性不是很好。可以先用interface或type来定义函数的类型,然后在定义函数的时候,指定默认值,这样阅读起来会清楚很多。28interface getFullNameType = {29 (firstName:string,middleName:string,lastName?:string):string30}31const getFullName: getFullNameType = function (fisrtName , lastName = '湖') {32 if (lastName) {33 return fisrtName + lastName34 } else {35 return fisrtName36 }37}38394041// 函数本身的类型,这里的 => 不是JS里面的箭头函数,而是定义返回add2的返回值类型, = add 这才是定义函数的操作。 2024.01.02这个理解有错误,这里的 => 就是箭头函数,指定了返回值类型。如果没有返回值,会写成这样 => void42// 这种定义方法是完整版的定义方法,代码非常不好看,用途暂时不明,一般不用,并不需要在函数名后面定义好参数和返回值类型。43// 注意这里的写法 add2: (x: number, y: number, z?:number) => number,这个 : 后面的 (x: number, y: number, z?:number) => number 是一个整体,用来定义函数参数和返回值类型。44const add2: (x: number, y: number, z?:number) => number = add454647// interface 描述函数类型48// 这里是箭头函数,赋值给了sum变量,并没有定义返回值类型。49const sum = (x: number, y: number) => {50 return x + y51}5253interface ISum {54 (x: number, y: number): number55}56const sum2: ISum = sum5758//剩余参数,扩展运算符,...args59const getSum = function (x: number, args: number[]): number {60 let arr = args;61 for (let i = 0; i < arr.length; i++) {62 x += arr[i];63 }64 return x;65};6667const p = getSum(1, 2,4,5,6,7);68console.log(p)// 25注意:
如果使用接口或type为函数添加类型,需要写成匿名函数的形式,也就是函数表达式的形式。
定义:函数名相同,而形参不同的多个函数。JS中没有函数重载,但TS和Java中存在此语法。
函数重载有什么好处呢?当用户用同一个函数名时,函数可以根据参数的不同返回不同的结果,这样就简化了代码。同时让别人在使用时,能够少记一些函数方法名,只需要根据不同的参数得到不同的结果,这样就简化了代码。
xxxxxxxxxx221// 重载函数声明。2function add(x: string, y: string): string;3function add(x: number, y: number): number;4// 为什么下面的函数中有了typeof来判断类型,这里还要加上函数重载?主要作用就是要TS来提示我们,在调用函数时,如果传参不符合要求,就给出提示。在调用阶段就给出提示,最大程度的避免了错误的产生。5// 因为如果没有重载函数声明,下面定义的函数是可以直接使用,但是如果传递过来的参数不符合x,y都是同一种类型的要求,那么ts就不会有提示(虽然函数这时候是没有执行结果的,但我们要求的是要有执行结果,不是吗?),也就没有起到限定类型的作用了。所以这里需要有重载函数声明。确实比较复杂。67// 定义函数实现8function add(x: string | number, y: string | number): string | number {9 //在实现的时候,要严格判断两个参数的类型是否相等10 if (typeof x === "string" && typeof y === "string") {11 return x + y;12 } else if (typeof x === "number" && typeof y === "number") {13 return x + y;14 }15}1617console.log(add(1, 2));18console.log(add("ok", "fine"));19/*输出:20321okfine22*/
如果我不想写函数重载,完全可以用类型断言来写,照样可以达到目的。
https://www.typescriptlang.org/docs/handbook/type-inference.html
没有明确的指定类型的时候,推测出一个类型。
xxxxxxxxxx31let txt = 1002txt = 'ok'3console.log(txt) //这时候会报错,因为txt首次赋值的时候是number,虽然没有明确的指出是number类型,但是ts会推论出txt是number类型,如果后面赋值为别的类型,会报错。https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-types
使用的时候,要使用联合类型的共同方法才能不报错,否则最好先判断类型再使用(这时候就要用到类型断言,否则会报错)。
xxxxxxxxxx81// 我们只需要用中竖线来分割两个类型2let numberOrString: number | string 34// 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:5numberOrString.toString()67// length属性只有string类型才有,number类型是没有的,所以要用到类型断言根据情况进行判断。8numberOrString.lengthhttps://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions
xxxxxxxxxx121// 这里我们可以用 as 关键字或者 <string>变量名,告诉 typescript 编译器,你没法判断我的代码,但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。2function getLength(input: string | number): number {3 // 类型断言4 const str = input as string5 6 if (str.length) {7 return str.length8 } else {9 const number = input as number10 return number.toString().length11 }12}类型断言的第二种写法:
xxxxxxxxxx71function getLength(input: string | number): number {2 if((<string>input).length){3 return (<string>input).length4 }else{5 return (<number>input).toString().length6 }7}就是使用typeof、instanceof、in先判断类型,然后就不用写另外一种类型判断,TS会帮助我们判断。这是一种简便方法。
xxxxxxxxxx81// typescript 在不同的条件分支里面,智能的缩小了范围,这样我们代码出错的几率就大大的降低了。2function getLength2(input: string | number): number {3 if (typeof input === 'string') {4 return input.length5 } else {//注意:这里并没有指明说else指的是什么,但是TS已经为我们缩小了范围,确定为number类型,这就称为类型守卫6 return input.toString().length7 }8}
面向对象编程的三大特点:
!!!!!!!!!!!!!!!!!!!!!!!!
JS中的类定义没有public、没有private、没有protected、没有readonly。
千万不要搞混了。
xxxxxxxxxx471class Animal {2 //定义属性。这个在JS中叫作“公有实例字段”,参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/Public_class_fields3 name: string;45 //定义构造函数:为了将来实例化对象的时候,可以直接对属性的值进行初始化6 constructor(name: string) {7 8 //更新对象中的属性数据9 this.name = name10 }11 public run() {12 return `${this.name} is running`13 }14}1516//实例化对象17const snake = new Animal('lily')181920// 继承的特性21class Dog extends Animal {22 constructor(name: string){23 //调用父类中的构造函数,使用super()24 super(name)25 }26 bark() {27 return `${this.name} is barking`28 }29}3031const xiaobao = new Dog('xiaobao')32console.log(xiaobao.run()) // xiaobao is running33console.log(xiaobao.bark()) //xiaobao is barking343536// 这里我们重写构造函数,注意在子类的构造函数中,必须使用 super 调用父类的方法,要不就会报错。(注意这里不是我以为的同名的方法中调用父类的方法才使用super,而是只要子类的构造函数中调用父类的方法,都要使用super,这是JS的知识,不是TS的知识)37class Cat extends Animal {38 constructor(name) {39 super(name)40 console.log(this.name)41 }42 run() {43 return 'Meow, ' + super.run()44 }45}46const maomao = new Cat('maomao')47console.log(maomao.run()) // Meow,maomao is running注意:在上面的代码中,实例化对象的时候,应该在对象后面指定类型,比如说const snake: Aniaml = new Animal('lily'),但是这个老师的文档里面没有写,这是TS的类型推论,并不代表可以不写,我最好还是写上去。

这个提示仔细一读,这里虽然定义的是class,但是ts将这个Animal类看作成了一种type,说明这就是和JS不一样的地方。
https://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers
作用:描述类中的成员(属性,构造函数,方法)的可访问性。
使用readonly修饰符将属性设置为只读,只读属性必须在声明时或构造函数里面被初始化。
xxxxxxxxxx181class Animal {2 3 //如果构造函数中没有对readonly修饰的属性成员进行初始化(就是如果没有constructor里面的这句代码 this.name = name),那么外部也是不能对这个属性值进行更改的:1、new一个实例的时候无法更改name,因为constructor里面没有这句代码;2、方法里面如果有更改的代码,而且constructor进行了初始化,方法里面更改时会报错(下面的say()方法)。4 readonly name: string='ok'5 6 // 在类的构造函数中,可以对readonly修饰的属性成员的数据进行修改。这里怎么理解?是new一个Animal对象的时候,赋初始值时改变了name的值,就是下面的link那个例子7 constructor(name: string){8 this.name = name9 }10 say(){11 this.name = 'kkk' //会报错,不能在类的普通方法中修改readonly修饰的成员属性值12 }13}14const dog = new Animal('link')15console.log(dog.name);// 输出 link1617dog.name = 'kk';// TS文件提示报错,说name是只读的,下面的代码都不会执行。18console.log(dog.name);// JS文件执行会输出 kk

回答上图中的问题:JS中可以这么写,但一般不需要预先对属性进行定义,而是直接在constructor里面进行定义。
xxxxxxxxxx151class Animal {2 3 // 构造函数中的name参数(注意:这里的name参数只是举例,事实上是任何参数),一旦使用readonly修饰后,那么该name参数就叫作参数属性(也就是省略了之前的 name:string 这句代码)。4 // 构造函数中的name参数,可以使用public、private、protected来修饰,修饰之后,在类中就有了一个name 的属性成员,这个成员属性的性质就是相应修饰符的性质5 constructor(readonly name: string='ok'){6 7 // 构造函数中的name参数,一旦使用readonly修饰后,那么Animal类中就有了一个name的属性成员,this.name = name这句代码就可以省略了。外部无法修改类中的name属性成员值8 // this.name = name // 这句代码可以省略9 }10}11const dog = new Animal('link')12console.log(dog.name);// 输出 link1314dog.name = 'kk';// TS文件提示报错,说name是只读的,下面的代码都不会执行。15console.log(dog.name);// JS文件执行会输出 kk
父类型的引用指向了子类型的对象(就是说在创建子类型的实例时,使用父类型来修饰),不同类型的对象针对相同的方法,产生了不同的行为。
这里的重点是:
1、使用父类型来修饰子类型的实例;
2、子类型中定义了同名的方法;
3、子类的实例使用同名方法时,会执行定义在子类型中的方法。
其实这里大部分都是JS类的知识,只不过添加了一些ts类型的功能。
xxxxxxxxxx621//定义一个父类2class Animal {3 name: string4 constructor(name: string){5 this.name = name6 }7 run(distance: number=0){8 console.log(`${this.name}跑了${distance}米`);9 }10}1112//定义一个子类13class Dog extends Animal {14 constructor(name: string){15 //调用父类的构造函数,实现子类中属性的实例化操作16 super(name)17 }18 run(distance: number=5){19 console.log(`${this.name}跑了${distance}米`);20 }21}2223//定义一个子类24class Pig extends Animal {25 constructor(name: string){26 super(name)27 }28 run(distance: number=10){29 console.log(`${this.name}跑了${distance}米`);30 }31}3233const dog: Dog = new Dog('大黄')34const pig: Pig = new Pig('小猪')35dog.run()36pig.run()37/*输出结果:38大黄跑了5米39小猪跑了10米40*/4142// 使用父类的类型来创建子类的对象,这里是重点,代码是否能够正常执行呢?43const dog: Animal = new Dog('大黄')44const pig: Animal = new Pig('小猪')45dog.run()46pig.run()47// 可以正常执行。48/*输出结果:49大黄跑了5米50小猪跑了10米51*/5253//定义一个函数,函数的参数类型是Animal类型的54function showRun(arg: Animal) {55 arg.run()56}57showRun(dog)58showRun(pig)59/*输出结果:60大黄跑了5米61小猪跑了10米62*/
TS支持通过getters/setters来截取对对象成员的访问,它能帮助你有效的控制对对象成员的访问。
注意:get和set本身就是js定义的关键字,有特定的作用。和其余的关键字,比如说class、function的是类似的,多用就会用了。这些都是JS的知识。
xxxxxxxxxx391class Person{2 firstName: string3 lastName: string4 constructor(firstName: string,lastName: string){5 this.firstName = firstName6 this.lastName = lastName7 }89 // 注意,fullName并没有预先定义,但是外界可以直接使用。10 // 读取器,负责读取数据的11 get fullName(){12 console.log('在get中哦');13 return this.firstName + ' ' + this.lastName14 }1516 // 设置器,负责设置(修改)数据的17 // 可能上面的get方法你感觉没什么特别,但是这个 set 方法你可以看一下,函数的参数val是怎么来的?说明set方法来修饰这个函数,就能够产生这个val参数。就是这样的写法。18 set fullName(val){19 console.log('在set中哦');20 let names = val.split(' ')21 this.firstName = names[0]22 this.lastName = names[1]23 }24}2526const p: Person = new Person('mike','jack')27console.log(p);28console.log(p.fullName);2930p.fullName = 'cheng long'31console.log(p.fullName);32/*输出结果:33Person { firstName: 'mike', lastName: 'jack' }34在get中哦35mike jack36在set中哦37在get中哦38cheng long39*/
静态成员:在类中通过static修饰的属性或方法,就是静态的属性或方法,使用的时候通过类名.的方式来调用。这和js用法是一致的。
xxxxxxxxxx161//静态属性,是类对象的属性。非静态属性,是类的实例对象的属性。2class Person{3 name: string='A'4 static name1: string = 'B'5}67console.log(Person.name1);8console.log(new Person().name);9/*输出结果:10B11A12*/1314console.log(Person.name);15// 实际上可以输出,输出结果是: Person 16// 这是为什么呢?因为name属性很特殊,并不表示可以用 类.非静态属性 来正常输出,只要把非静态属性名改为别的变量名,就会看到报错。
抽象类作为其它派生类的基类(父类)使用,它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。abstract关键字用于定义抽象类和在抽象类中定义抽象方法。
xxxxxxxxxx231/*2抽象类不能创建实例对象,只有实现类才能创建实例。可以包含未实现的抽象方法。3*/4abstract class Animal{5 abstract cry(): any;6 run(){7 console.log('run()');8 }9}1011class Dog extends Animal{12 cry(){13 console.log('Dog cry()');14 }15}1617const dog = new Dog()18dog.cry()19dog.run()20/*输出结果:21Dog cry()22run()23*/
类本身可以当作一种类型,这里与接口结合,是为什么呢?从下面的例子中可以看到,接口其实是定义好了方法的类型,那么类与接口结合,就可以很简单地实现类中方法的类型定义,而不用另外写一遍。
xxxxxxxxxx481//类可以通过接口的方式,来定义当前这个类的类型,注意:接口中的内容都要真正的实现。23//定义一个接口,这里实际上只是定义了类的一个方法4interface Radio {5 // 定义一个方法6 /*7 这一点在函数那里可以看到,函数接口里面是参数和返回值的类型定义8 interface IPerson{9 (x:number,y:number):number10 }11 定义类的方法,就要加上方法名12 */13 switchRadio(trigger: boolean): void;14}1516//定义一个类,这个类的类型就是定义的接口Radio17class Car implements Radio {18 switchRadio(trigger) {19 console.log('123')20 }21}2223class Cellphone implements Radio {24 switchRadio() {25 }26}2728interface Battery {29 checkBatteryStatus(): void;30}3132// 要实现多个接口,我们只需要中间用 逗号 隔开即可。33class Cellphone implements Radio, Battery {34 switchRadio() {35 }36 checkBatteryStatus() {37 }38}3940//有时候,一个类需要实现多个接口,使用 逗号 隔开太复杂了,此时可以定义一个接口,用来继承其它接口,那么这个类只需要实现这个接口就行了41interface Combine extends Radio, Battery{}4243class CellPhone implements Combine{44 switchRadio() {45 }46 checkBatteryStatus() {47 } 48}这里的例子好像没有报错,new Car().switchRadio(1);在ts文件中并没有进行报错,不应该这样啊,虽然说Car这个类的switchRadio方法并没有规定类型,但已经implements Radio了,应该有类型提示啊,找原课程看一下。
上面这里的意思是说:
执行
new Person().switchRadio(1)没有报错,我的想法是不应该的。但实际上是可以的,因为ts已最具体的方法为主,虽然implements的Radio接口上参数有类型,但是实际定义的类方法上参数没有类型,那么使用implements的时候,代码提示都要求写完整的代码。
定义了Radio接口,好像唯一的好处就是,在写这个方法的时候,可以有提示直接写出来,但必须写完整,否则new Car().switchRadio(1);是不会有报错的。
是的,整个typescript不就是这个功能吗,让你写代码更规范。
如果使用了implements,则必须要在类里面写implements过来的方法,否则会报错。

2024.01.03
那能不能直接在类中的方法上定义类型呢?而不是使用implements关键字?
可以,只不过接口里面不需要写方法名,并且类方法要写成匿名函数(函数表达式)的形式。
写成命名函数的形式是不行的:
https://www.typescriptlang.org/docs/handbook/enums.html
就像名字那样,是为了一个个的举例来使用的。
!!!!!!!!!!!!!!!!!!!!!!!!
ts不是讲类型吗?这个枚举是类型吗?很明显不是,在我看来这个只是一种附加功能。
xxxxxxxxxx251// 数字枚举,一个数字枚举可以用 enum 这个关键词来定义,我们定义一系列的方向,然后这里面的值,枚举成员会被赋值为从 0 开始递增的数字。2enum Direction {3 Up,4 Down,5 Left,6 Right,7}8console.log(Direction.Up) //输出 0910// 还有一个神奇的点是这个枚举还做了反向映射11console.log(Direction[0]) //输出 Up1213// 字符串枚举14enum Direction {15 Up = 'UP',16 Down = 'DOWN',17 Left = 'LEFT',18 Right = 'RIGHT',19}20const value = 'UP'21if (value === Direction.Up) {22 console.log('go up!')23}2425const direction:Direction = 'Up'; // 报错,说明枚举本质上是一种类型。
https://www.typescriptlang.org/docs/handbook/generics.html
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
这个方法让我们能够在使用的时候,清楚的知道我定义了什么类型,然后应该传入什么样类型的数据,这就非常灵活好用了。(但是定义起来非常复杂、麻烦)
这个不要和类型断言的第二种用法搞混了,类型断言是
<类型>变量名,而泛型里面是函数名<T>。!!!!!!!!!!!!!!!!!!!!!!!!
仔细看一看,泛型定义和普通的函数(这里以函数来举例,其实还有类、接口等等都可以用到泛型)定义有什么区别呢?
1、泛型定义的函数,函数名后面加上了泛型定义,function 函数名
(参数名: T): T{} 。而普通函数的定义没有泛型定义,function 函数名(参数名: 数据类型): 数据类型{}。2、使用的时候,泛型定义的函数,需要在函数名后面指定数据类型,const result = echo
(123) ,这时候ts会根据函数使用时定义的类型来提示。普通函数使用时,和JS一样。

2024.01.02
上面这段话的第一行不准确:“这个不要和类型断言的第二种用法搞混了,类型断言是
<类型>变量名,而泛型里面是函数名<T>”。泛型里面不只是
函数名<T>,还有接口<T>、类<T>、类型别名<T>这些写法,详见https://wangdoc.com/typescript/generics。
xxxxxxxxxx211function echo(arg) {2 return arg3}4const result = echo(123)5// 这时候我们发现了一个问题,我们传入了数字,但是返回了 any,如果是any类型的话,后面使用方法如果出错,就没有任何静态提示信息,而我们需要提示信息。678// <T> 是默认的写法,但可以是任意大写字母,这就是泛型的标识,其余部分和定义具体类型一样写即可。说实话,就是为了能够用到静态提示功能。9// 这里在三个位置都用到了T,是不是必须规定成这样呢?不是的,应该是要用到泛型的地方,就用T,不用的地方可以定义为固定类型或者不定义。函数名后面的 <T> 只是显式地指出了函数正在使用泛型。然后函数里的参数和返回值都可以指定为泛型类型。 10function echo<T>(arg: T): T {11 return arg12}13const result = echo<number>(123)1415// 泛型也可以传入多个值。16// 这里为什么用到了[]?是泛型的用法吗?不是,是 2-3 中元组类型定义的方法。17function swap<T, U>(tuple: [T, U]): [U, T] {18 return [tuple[1], tuple[0]]19}2021const result = swap<string, number>(['string', 123])

上面这张图的注解有一个错误,就是泛型函数在使用的时候,可以在函数名后面加上泛型,也可以不加上泛型,如果不加,ts会使用类型推断。
箭头函数的泛型写法:
xxxxxxxxxx11const getObjectKeys = <T>(obj:T) => Object.keys(obj) as (keyof T)[]测试一下:
以后无论哪种写法,都要会。
泛型的符号T指代的含义其实很灵活,上面的只是最简单的例子,将T指定为number或者string类型,其实T完全可以指定为接口、另一个使用了泛型定义的类型、数组类型等等,这中用法一定要会:
输出内容:
只要我习惯看泛型的嵌套,ts的难度就会降低很多了。
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法。定义一个接口来约束泛型。
从上一个小节的例子可以看到,定义的类型要么是直接返回参数,要么就是直接定义某种类型。没有涉及到在函数里面对泛型参数进行处理。
那么在函数里面对泛型参数进行处理,这时候T表示任意类型,太宽泛了,无法保证这个参数有你想要的属性,ts这时候就会提示
Property 'xxx' does not exist on type 'T'.,这时候就需要使用泛型约束来收缩类型,这样ts就能够知道,使用你指定的属性是没有问题的。
案例:
xxxxxxxxxx41function echoWithArr<T>(arg: T): T {2 console.log(arg.length)3 return arg4}上例中,泛型 T 不一定包含属性 length,我们在使用时可以给他传入任意类型,当然有些不包括 length 属性,那样就会报错。所以此时ts会提示我们Property 'length' does not exist on type 'Type'.。
xxxxxxxxxx41function f<T>(value: T[]): T {2 console.log(value.length)3 return value[0]4}把类型改为T[],这就表示value一定存在length属性,因此就可以访问了。
extends 来约束泛型extends:延伸;扩充;继承。在ts里面有多种含义。
在 TypeScript(TS)中,
extends是核心关键字,核心含义围绕 “继承” 和 “约束” 两大场景,具体用法完全贴合 TS 的类型系统特性,以下是最常用、最核心的 4 种含义(按使用频率排序):1. 类(Class)继承:直接对应 “继承”(和 Java 逻辑一致)
这是最贴近 OOP 基础的用法,和你之前了解的 Java 中
extends含义完全相同 —— 表示子类继承父类,子类会获得父类的属性、方法(可重写),且 TS 中类只能单继承(一个子类只能extends一个父类)。示例代码:
xxxxxxxxxx261// 父类:基础动物类2class Animal {3name: string;4constructor(name: string) {5this.name = name;6}7eat() {8console.log(`${this.name} 在吃东西`);9}10}1112// 子类:Dog 继承自 Animal(extends = 继承)13class Dog extends Animal {14// 子类可新增自己的属性/方法15bark() {16console.log(`${this.name} 在汪汪叫`);17}18// 子类可重写父类方法19override eat() { // TS 4.3+ 推荐用 override 明确标记重写20console.log(`${this.name} 在吃骨头`);21}22}2324const dog = new Dog("旺财");25dog.eat(); // 输出:旺财在吃骨头(重写后的方法)26dog.bark(); // 输出:旺财在汪汪叫(子类新增方法)2. 接口(Interface)扩展:对应 “扩展”(复用 + 新增类型约束)
TS 中接口(
interface)用extends表示扩展已有接口—— 本质是 “复用原有接口的类型约束,再新增自己的约束”,支持单扩展或多扩展(多个接口用逗号分隔)。示例代码:
xxxxxxxxxx241// 基础接口:用户的核心信息2interface User {3id: number;4name: string;5}67// 扩展接口:管理员(继承 User 的约束,再新增 role 约束)8interface Admin extends User { // extends = 扩展9role: "admin"; // 管理员专属角色10manageUsers(): void; // 管理员专属方法11}1213// 多扩展:超级管理员(同时继承 User 和 Admin 的约束)14interface SuperAdmin extends User, Admin {15superPower: string;16}1718// 类型校验:必须包含 User + Admin 的所有约束19const admin: Admin = {20id: 1,21name: "张三",22role: "admin",23manageUsers: () => console.log("管理用户")24};3. 泛型(Generic)约束:对应 “约束 / 限定”(缩小泛型范围)
这是 TS 特有的核心用法 —— 用
extends限制泛型的可选类型范围,确保泛型参数必须满足某个条件(比如继承某个类、实现某个接口),避免传入不合法的类型。示例代码:
xxxxxxxxxx111// 泛型 T 必须 extends User(约束 T 是 User 或其扩展类型)2function getUserInfo<T extends User>(user: T): T {3console.log(`用户 ID:${user.id},姓名:${user.name}`);4return user;5}67// 合法:Admin 是 User 的扩展类型,满足约束8getUserInfo(admin);910// 非法:传入的对象没有 id/name,不满足 User 约束(TS 编译报错)11getUserInfo({ age: 20 });常见延伸:泛型条件类型(
T extends U ? X : Y)
extends还用于 “类型三元表达式”,表示 “如果 T 满足 U 的约束,则返回类型 X,否则返回 Y”,是 TS 高级类型(如Exclude、Extract)的实现基础。示例:
xxxxxxxxxx51// 条件类型:如果 T 是 string,返回 number,否则返回 boolean2type FormatType<T> = T extends string ? number : boolean;34type A = FormatType<string>; // A = number(满足约束)5type B = FormatType<number>; // B = boolean(不满足约束)4. 模块扩展(Module Augmentation):对应 “扩展”(补充模块类型)
TS 中可通过
extends扩展已有模块 / 第三方库的类型定义(比如给Array、Vue等内置 / 第三方类型新增方法 / 属性),解决 “第三方库类型缺失” 的问题。示例代码(扩展数组类型):
xxxxxxxxxx181// 扩展内置的 Array 模块,新增 sum 方法的类型定义2declare global {3interface Array<T> extends Array<T> { // extends = 扩展原有 Array 类型4sum(): T extends number ? number : never; // 仅当数组元素是 number 时,sum 返回 number5}6}78// 实现 sum 方法(类型定义 + 实现分离)9Array.prototype.sum = function() {10if (this.every(item => typeof item === "number")) {11return this.reduce((a, b) => a + b, 0);12}13throw new Error("仅数字数组支持 sum 方法");14};1516// 使用:TS 能识别 sum 方法的类型(返回 number)17const numArr = [1, 2, 3];18console.log(numArr.sum()); // 6(类型正确)
| 使用场景 | 含义 | 核心目的 |
|---|---|---|
| 类(Class)继承 | 继承 | 子类复用父类的属性 / 方法 |
| 接口(Interface)扩展 | 扩展 | 复用原有接口,新增类型约束 |
| 泛型(Generic)约束 | 约束 / 限定 | 缩小泛型的合法类型范围 |
| 泛型条件类型 | 满足约束? | 基于类型约束判断返回目标类型 |
| 模块扩展(Module Aug) | 扩展 | 补充已有模块的类型定义 |
xxxxxxxxxx151interface IWithLength {2 // 接口中有一个length属性3 length: number;4}56function echoWithLength<T extends IWithLength>(arg: T): T {7 console.log(arg.length)8 return arg9}1011echoWithLength('str');// 312const result3 = echoWithLength({length: 10});// 1013const result4 = echoWithLength([1, 2, 3]);// 31415console.log(echoWithLength(123));// 报错这里的T extends IWithLength可以理解为“约束T必须为IWithLength类型或者其扩展类型”。这样理解就很清晰了。


泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量的约束)。
比如,创建一个函数来获取对象中属性的值:
xxxxxxxxxx61// T[Key] 是TS中的索引访问类型,后面会讲到2function getProp<T, Key extends keyof T>(obj: T, key: Key): T[Key] {3 return obj[key];4}56getProp({ name: "jack", age: 18}, "age");
泛型接口:在定义接口时,为接口中的属性或方法定义泛型类型,在使用接口时,再指定具体的泛型类型。
xxxxxxxxxx81// 泛型 和 interface2interface KeyPair<T, U> {3 key: T;4 value: U;5}67let kp1: KeyPair<number, string> = { key: 1, value: "str"}8let kp2: KeyPair<string, number> = { key: "str", value: 123}另外一个例子:
xxxxxxxxxx131// 定义接口类型2interface IdFunc<T> {3 id: (value: T) => T4 ids: () => T[]5}67// 使用接口类型8let obj: IdFunc<number> = {9 id: (value) => {10 return value;11 },12 ids: () => [1, 3, 5]13}
这个只是说明ts中有这种功能,我在使用的时候注意一下即可,不需要我做什么。
JS的数组在TS中就是一个泛型接口,当我们在使用数组时,TS会根据数组的不同类型,来自动将类型变量设置为相应的类型。
比如说我定义了一个数组const strs = ['a','b','c'];,当写出strs.的时候,就会给出提示,能够使用哪些方法。

并且,当我使用某个方法之后,可以将鼠标悬浮到这个方法上面,来查看这个方法的具体的类型定义。

泛型与类:
与泛型定义函数一样,需要先在类上定义好类型:class 类名
{} ,然后类里面可以使用泛型T。
xxxxxxxxxx401// 为类定义泛型2class Queue {3 private data = [];4 push(item) {5 return this.data.push(item)6 }7 pop() {8 return this.data.shift()9 }10}1112const queue = new Queue()13queue.push(1)14queue.push('str')15console.log(queue.pop().toFixed())16console.log(queue.pop().toFixed())1718//在上述代码中存在一个问题,它允许你向队列中添加任何类型的数据,当然,当数据被弹出队列时,也可以是任意类型。在上面的示例中,看起来人们可以向队列中添加string 类型的数据,但是在使用的过程中,就会出现我们无法捕捉到的错误。toFixed()方法是四舍五入,只能用于number,如果是string类型就会报错。1920class Queue<T> {21 private data = [];22 push(item: T) {23 return this.data.push(item)24 }25 pop(): T {26 return this.data.shift()27 }28}29const queue = new Queue<number>()3031// 上面的类定义会报错,下面是不会报错的版本32class Queue<T> {33 private data: T[] = [];34 push(item: T) {35 return this.data.push(item);36 }37 pop(): T | undefined {38 return this.data.shift();39 }40}2024.01.03
在定义泛型的时候,使用了T、U,脑袋还是很混乱,说明之前我学习react hooks的时候是没有搞清楚用法的。
这里的用法很明显是在使用,所以没有看见T、U这些抽象符号。至于为什么这里可以使用泛型,肯定是看了文档的,这里指定了泛型,所以可以用。
TS内置了一些常用的工具类型,来简化TS中的一些常见的操作。
说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。
这些工具类型有很多,现在只学习下面几个。
1、Partial<Type>
作用:用来构造(创建)一个类型,将Type的所有属性设置为可选。
xxxxxxxxxx61interface Props {2 id: string;3 children: number[];4}56type PartialProps = Partial<Props>;构造出来的新类型PartialProps结构与Props相同,但所有属性都变为可选的。

2、Readonly<Type>
作用:构造一个类型,将Type的所有属性都设置为readonly(只读)。
xxxxxxxxxx61interface Props {2 id: string;3 children: number[];4}56type ReadonlyProps = Readonly<Props>;构造出来的ReadonlyProps结构与Props相同,但所有属性都变为只读的。
当我们想赋值时,就会报错:


3、Pick<Type, Keys>
从Type中选择一组属性来构造新类型。
xxxxxxxxxx71interface Props {2 id: string;3 title: string;4 children: number[];5}67type PickProps = Pick<Props, "id" | "title">;

4、Record<Keys, Type>
构造一个对象类型,属性键为Keys,属性类型为Type。
这句话要理解清楚,首先结果是生成一个对象类型,那么对象类型里面的key就从Keys里面来,那么key的类型全部为Type类型。看下面这个案例就清楚了。
xxxxxxxxxx11type RecordObj = Record<'a' | 'b' | 'c', string[]>那么RecordObj的类型结构就是下面这样:

